/**
 * @project ginLogrus
 * @author JurenGo
 * @desc 为gin创建两个日志:一个是access.log是gin middleware,无需调用,直接记录访问信息
 *       另外一个是业务日志trace.log,用户自己的代码中手工调用,记录业务信息和error call stacktrace
 *       错误的call stack的记录由errors插件来实现
 *       另外,可以设置error.log文件单独记录warning以上级别的信息,这样错误日志可以保存更长时间
 *		 logrus的日志输出级别,是根据gin.Mode()来记录,如果gin.DebugMode,则logrus的输出级别为debug,否则为info
 * @date 8:40 AM 1/3/23
 **/

package ginLogrus

import (
	"fmt"
	"github.com/gin-gonic/gin"
	//日志切换插件,可以根据日志创建时间切换日志,还可以设置日志文件保存的数量,和为活动日志创建没有时间戳的file link
	rotateLogs "github.com/lestrrat-go/file-rotatelogs"
	//出错时记录call stack 输出格式 %+v
	"github.com/pkg/errors"
	//logrus的钩子插件,可以根据loglevel记录到不同的文件
	fileLogHook "github.com/rifflock/lfshook"
	//logrus日志插件
	"github.com/sirupsen/logrus"
	//设置日志文件头格式:时间格式,颜色格式,可以实现stdout有颜色,log file无颜色
	prefixFormatter "github.com/x-cray/logrus-prefixed-formatter"
	"net/http"
	"os"
	//类型映射,golang就麻烦在这里
	"reflect"
	"time"
)

const (
	errLogPrefix = "error.log"
	errLogSuffix = "%Y%m"
)

// LogAccessConfig
/*
 * @msg access.log config info
 */
type LogAccessConfig struct {
	StdTimeFormat  string
	FileTimeFormat string
	LogPath        string
	FilePrefix     string
	FileSuffix     string
	SkipRoute      map[string]struct{}
	IsErrFileHook  bool
	KeepCount      int
}

// LogTraceConfig
/*
 * @msg trace log config info
 */
type LogTraceConfig struct {
	StdTimeFormat  string
	FileTimeFormat string
	LogPath        string
	FilePrefix     string
	FileSuffix     string
	IsErrFileHook  bool
	KeepCount      int
}

// NewAccessLog
/*
 * @msg gin的日志middleware,gin系统自动调用
 * @param logConfig *LogAccessConfig
 * @return: gin.HandlerFunc   //标准gin middleware返回结构体
 */
func NewAccessLog(logConfig *LogAccessConfig) gin.HandlerFunc {
	log := logrus.New()
	if err := configLog(log, logConfig); err != nil {
		log.Panicf("%+v", err)
	}

	return func(ctx *gin.Context) {
		if _, ok := logConfig.SkipRoute[ctx.Request.URL.Path]; ok {
			return
		}
		start := time.Now()
		path := ctx.Request.URL.Path
		raw := ctx.Request.URL.RawQuery
		ctx.Next()
		end := time.Now()
		latency := end.Sub(start) //记录请求处理时间
		clientIP := ctx.ClientIP()
		method := ctx.Request.Method
		statusCode := ctx.Writer.Status()
		//请求大小
		bodySize := ctx.Writer.Size()

		//记录url param
		if raw != "" {
			path = path + "?" + raw
		}
		//设置json字段内容
		entry := log.WithFields(logrus.Fields{
			"statusCode": statusCode,
			"latency":    latency, // time to process
			"clientIP":   clientIP,
			"method":     method,
			"path":       path,
			"dataLength": bodySize,
		})

		if len(ctx.Errors) > 0 {
			entry.Error(ctx.Errors.ByType(gin.ErrorTypePrivate).String())
		} else {
			//msg := fmt.Sprintf("%s - \"%s %s\" %d %d (%dms)", clientIP, method, path, statusCode, bodySize, latency)
			if statusCode >= http.StatusInternalServerError { //返回码500自动error级别
				entry.Error()
			} else if statusCode >= http.StatusBadRequest { //400返回码,自动warn级别
				entry.Warn()
			} else {
				entry.Info()
			}
		}

	}
}

func NewTraceLog(logConfig *LogTraceConfig) *logrus.Logger {
	log := logrus.New()
	if err := configLog(log, logConfig); err != nil {
		log.Panicf("%+v", err)
	}
	return log
}

func configLog(log *logrus.Logger, logConfig any) error {
	//var mapSkip map[string]struct{}
	var keepCount int
	var isErrFileHook bool
	var mapConfig = make(map[string]string)
	//logConfig是pointer,所以case对应该是pointer
	switch logConfig.(type) {
	case *LogAccessConfig:
		//nothing
	case *LogTraceConfig:
		//nothing
	default:
		return errors.Errorf("unknown log config type %T", logConfig)
	}
	//Elem()可以取pointer指向的值
	stValue := reflect.ValueOf(logConfig).Elem()
	stKey := reflect.TypeOf(logConfig).Elem()

	for i := 0; i < stValue.NumField(); i++ {
		switch stValue.Field(i).Kind() {
		case reflect.Int:
			keepCount = int(stValue.Field(i).Int())
		case reflect.String:
			mapConfig[stKey.Field(i).Name] = stValue.Field(i).String()
		case reflect.Bool:
			isErrFileHook = stValue.Field(i).Bool()
		case reflect.Map:
		// 什么也不做,但需要列出来,以免进入default
		// MapRange()不能用range,只能用for
		//	for iter := stValue.Field(i).MapRange(); iter.Next(); {
		//		mapSkip[iter.Key().String()] = struct{}{}
		//	}
		default:
			return errors.Errorf("unknown type %s of struct %s", stValue.Field(i).Kind().String(), stKey.Name())
		}
	}
	//根据gin的mode来设置logrus的loglevel
	if gin.Mode() == gin.DebugMode {
		log.SetLevel(logrus.DebugLevel)
	} else { //Release Mode
		log.SetLevel(logrus.InfoLevel)
	}

	//主要设置stdout有颜色,file中必须没有颜色标识符
	stdFormatter := &prefixFormatter.TextFormatter{
		FullTimestamp:   true,
		TimestampFormat: mapConfig["StdTimeFormat"], //设置时间格式
		ForceFormatting: true,
		ForceColors:     true,
		DisableColors:   false,
	}
	log.SetFormatter(stdFormatter) //设置log格式
	fileFormatter := &prefixFormatter.TextFormatter{
		FullTimestamp:   true,
		TimestampFormat: mapConfig["FileTimeFormat"], //log file中设置长时间格式
		ForceFormatting: true,
		ForceColors:     false,
		DisableColors:   true,
	}

	//目录结构一定是755否则,不能创建trace.log,access.log,error.log链接
	if err := os.MkdirAll(mapConfig["LogPath"], 0775); err != nil {
		return errors.Wrapf(err, "日志目录:%s,创建失败!", mapConfig["LogPath"])
	}
	//拼装full log name
	FilePrefix := fmt.Sprintf("%s%s", mapConfig["LogPath"], mapConfig["FilePrefix"])

	//设置log切换规则
	logWriter, err := rotateLogs.New(fmt.Sprintf("%s.%s", FilePrefix, mapConfig["FileSuffix"]),
		rotateLogs.WithLinkName(FilePrefix),           //为当前有时间戳的日志创建无时间戳的link
		rotateLogs.WithMaxAge(-1),                     //禁止根据文件创建时间删除日志
		rotateLogs.WithRotationCount(uint(keepCount)), //设置根据日志数量来保留日志
	)

	//文件创建失败抛出错误
	if err != nil {
		return errors.Cause(err)
	}

	//钩子用处是默认在终端显示logrus信息的同时,触发fire,调用钩子记录同样的信息到日志文件
	//设置日志文件的钩子句柄,可以根据日志级别设置不同的文件句柄
	logHook := fileLogHook.NewHook(fileLogHook.WriterMap{
		logrus.DebugLevel: logWriter,
		logrus.InfoLevel:  logWriter,
		logrus.WarnLevel:  logWriter,
		logrus.ErrorLevel: logWriter,
		logrus.FatalLevel: logWriter,
	}, fileFormatter)

	//添加日志文件钩子配置
	log.AddHook(logHook)

	//如果需要单独记录warning以上级的信息到error.log
	//为error.log日志创建钩子
	if isErrFileHook {
		//全路径日志前缀
		FilePrefix = fmt.Sprintf("%s%s", mapConfig["LogPath"], errLogPrefix)
		//error.log句柄
		//因为errLogSuffix为"%Y%m",所以error.log名字是按月份分割
		//所以如果keeCount为7,则可以保留7个月的warning以上级别的日志
		errWriter, err := rotateLogs.New(fmt.Sprintf("%s.%s", FilePrefix, errLogSuffix),
			rotateLogs.WithLinkName(FilePrefix),
			rotateLogs.WithMaxAge(-1),
			rotateLogs.WithRotationCount(uint(keepCount)), //日志保留7个
		)

		if err != nil {
			return errors.Cause(err)
		}
		//error.log为warning以上级别
		errHook := fileLogHook.NewHook(fileLogHook.WriterMap{
			logrus.WarnLevel:  errWriter,
			logrus.ErrorLevel: errWriter,
			logrus.FatalLevel: errWriter,
		}, fileFormatter)

		log.AddHook(errHook)
	}

	return nil
}
